What if the Netherlands used FPTP?

politics mapview ggpol

Examining the difference in election outcome between FPTP and D’Hondt (proportional representation).

Thomas Zwagerman https://twitter.com/thomzwa (Centre for Ecology & Hydrology)https://www.ceh.ac.uk/
03-24-2021
Show code
knitr::include_graphics("preview.jpg")

In this short article I am exploring what the Dutch political landscape would look like if it operated under a first-past-the-post (FPTP) system.

The election data for the Netherlands was downloaded from the Kiesraad. The shapefile was downloaded from ESRI NL.

Results Tweede Kamer Verkiezing 2021

These are the 2021 results summarised:

Show code
#sort in long format
df_results[is.na(df_results)] <- 0

df_pr <- df_results %>% 
  pivot_longer(cols = VVD:Modern.Nederland, names_to = "party", values_to = "votes") %>%
  select(party, votes) %>% 
  group_by(party) %>% 
  summarise(votes=sum(votes))

df_pr$party <- gsub("Partij.van.de.Arbeid..P.v.d.A..","PvdA",df_pr$party)
df_pr$party <- gsub("Democraten.66..D66.","D66",df_pr$party)
df_pr$party <- gsub("X50PLUS","Partij 50PLUS",df_pr$party)
df_pr$party <- gsub("Forum.voor.Democratie","FvD",df_pr$party)
df_pr$party <- gsub("Forum.voor.Democratie","FvD",df_pr$party)
df_pr$party <- gsub("SP..Socialistische.Partij.","SP",df_pr$party)
df_pr$party <- gsub("PVV..Partij.voor.de.Vrijheid.","PVV",df_pr$party)
df_pr$party <- gsub("Staatkundig.Gereformeerde.Partij..SGP.","SGP",df_pr$party)
df_pr$party <- gsub("Partij.voor.de.Dieren","PvdD",df_pr$party)

dHondt <- function(votes, parties, n_seats = 150) {
  
  divisor.mat           <- sum(votes) / sapply(votes, "/", seq(1, n_seats, 1))
  colnames(divisor.mat) <- parties
  
  m.mat     <- tidyr::gather(as.data.frame(divisor.mat), key="name", value="value",
                             everything())
  m.mat     <- m.mat[rank(m.mat$value, ties.method = "random") <= n_seats, ]
  rle.seats <- rle(as.character(m.mat$name))
  
  if (sum(rle.seats$length) != n_seats)
    stop(paste("Number of seats distributed not equal to", n_seats))
  
  # fill up the vector with parties that got no seats
  if (any(!(parties %in% rle.seats$values))) {
    # add parties
    missing_parties <- parties[!(parties %in% rle.seats$values)]
    for (party in missing_parties) {
      rle.seats$lengths <- c(rle.seats$lengths, 0)
      rle.seats$values  <- c(rle.seats$values, party)
    }
    # sort results
    rle.seats$lengths <- rle.seats$lengths[match(parties, rle.seats$values)]
    rle.seats$values  <- rle.seats$values[match(parties, rle.seats$values)]
  }
  
  rle.seats$length
  
}

df_pr$seats<- dHondt(df_pr$votes,df_pr$party,150)
df_pr <- df_pr %>% 
  filter(seats>0) %>% 
  as.data.frame()
df_pr <- df_pr[order(df_pr$votes,decreasing =T),] 
df_pr <- left_join(df_pr,colourscheme)

df_pr$total <- sum(df_pr$votes)
df_pr$percentage <- (df_pr$votes/df_pr$total)*100
df_pr$percentage <- round(df_pr$percentage,1)

resultaat <- df_pr %>% 
  select(party,votes,seats,percentage)
kable(resultaat)
party votes seats percentage
VVD 2279126 34 22.3
D66 1565862 24 15.3
PVV 1124482 17 11.0
CDA 990601 15 9.7
SP 623371 9 6.1
PvdA 597192 9 5.8
GROENLINKS 537308 8 5.3
FvD 523083 8 5.1
PvdD 399751 6 3.9
ChristenUnie 351275 5 3.4
Volt 252480 3 2.5
JA21 246620 3 2.4
SGP 215249 3 2.1
DENK 211238 3 2.1
Partij 50PLUS 106702 1 1.0
BBB 104319 1 1.0
BIJ1 87238 1 0.9

Let’s start with examining the composition of the Tweede Kamer - under a proportional representation (D’Hondt) system. This is what the Tweede Kamer looks like currently:

Show code
ggplot(df_pr) +
  ggpol::geom_parliament(aes(seats = seats, fill = party),color="black") + 
  #highlight the party in control of the House with a black line
  scale_fill_manual(values = df_pr$colour, 
                    labels = df_pr$party)+
  coord_fixed()+
  theme_void()

Now let’s have a look at what it would look like under First-past-the-Post (FPTP), with one MP elected per Gemeente.

Show code
#sort in long format
df_long <- df_results %>% 
  pivot_longer(cols = VVD:Modern.Nederland, names_to = "party", values_to = "votes") %>%
  select(regio =RegioNaam, code = RegioCode,party, votes) %>% 
  filter(!is.na(votes)) %>% 
  group_by(code) %>% 
  mutate(total = sum(votes)) %>% 
  slice_max(votes)

df_long$percentage <- (df_long$votes/df_long$total)*100
df_long$percentage <- round(df_long$percentage,1)

df_long$code <- gsub("G","",df_long$code)

#fix the names
df_long$party <- gsub("Partij.van.de.Arbeid..P.v.d.A..","PvdA",df_long$party)
df_long$party <- gsub("Democraten.66..D66.","D66",df_long$party)
df_long$party <- gsub("X50PLUS","Partij 50PLUS",df_long$party)
df_long$party <- gsub("Forum.voor.Democratie","FvD",df_long$party)
df_long$party <- gsub("Forum.voor.Democratie","FvD",df_long$party)
df_long$party <- gsub("SP..Socialistische.Partij.","SP",df_long$party)
df_long$party <- gsub("PVV..Partij.voor.de.Vrijheid.","PVV",df_long$party)
df_long$party <- gsub("Staatkundig.Gereformeerde.Partij..SGP.","SGP",df_long$party)
df_long$party <- gsub("Partij.voor.de.Dieren","PvdD",df_long$party)
df_long$party <- as.factor(df_long$party)
df_long$regio <- gsub("'","",df_long$regio)
df_long$regio <- gsub("-"," ",df_long$regio)
df_long <- left_join(df_long,colourscheme)


fptp_results <- table(df_long$party) %>% as.data.frame()
colnames(fptp_results) <- c("party","seats")
fptp_results <- left_join(fptp_results,colourscheme)

ggplot(fptp_results) +
  ggpol::geom_parliament(aes(seats = seats, fill = party),color="black") + 
  #highlight the party in control of the House with a black line
  scale_fill_manual(values = fptp_results$colour, 
                    labels = fptp_results$party)+
  coord_fixed()+
  theme_void()

Now, there are a couple of obvious differences:

But also some obvious caveats:

Election results on a map

We can also show these results on a map - this in an interactive map which allows you to explore the individual election results for each Gemeente.

Show code
#sort in long format
df_pie <- df_results %>% 
  pivot_longer(cols = VVD:Modern.Nederland, names_to = "party", values_to = "votes") %>%
  select(regio =RegioNaam, code = RegioCode,party, votes) %>% 
  filter(!is.na(votes)) %>% 
  group_by(code) %>% 
  mutate(total = sum(votes)) 
df_pie$code <- gsub("G","",df_pie$code)

names_df <- unique(df_pie$code)
names_shp <- unique(shp_gemeentes$Gemeenteco)
names_remove <- names_df[!names_df %in% names_shp]
#remove Bonaire, Saba, Sint Eustasius
df_pie <- df_pie %>% 
  filter(!code %in% names_remove)
df_pie$percentage <- (df_pie$votes/df_pie$total)*100
df_pie$percentage <- round(df_pie$percentage,1)
# 
df_pie$party <- gsub("Partij.van.de.Arbeid..P.v.d.A..","PvdA",df_pie$party)
df_pie$party <- gsub("Democraten.66..D66.","D66",df_pie$party)
df_pie$party <- gsub("X50PLUS","Partij 50PLUS",df_pie$party)
df_pie$party <- gsub("Forum.voor.Democratie","FvD",df_pie$party)
df_pie$party <- gsub("Forum.voor.Democratie","FvD",df_pie$party)
df_pie$party <- gsub("SP..Socialistische.Partij.","SP",df_pie$party)
df_pie$party <- gsub("PVV..Partij.voor.de.Vrijheid.","PVV",df_pie$party)
df_pie$party <- gsub("Staatkundig.Gereformeerde.Partij..SGP.","SGP",df_pie$party)
df_pie$party <- gsub("Partij.voor.de.Dieren","PvdD",df_pie$party)
df_pie$party <- as.factor(df_pie$party)

df_pie$party <- as.character(df_pie$party)
df_pie$percentage <- as.numeric(df_pie$percentage)

pie_plot_list <- lapply(unique(df_pie$code), function(i) {
  regio_df <- df_pie %>% 
    filter(code == i) %>%
    dplyr::ungroup() %>% 
    select(party,percentage,regio) %>%
    filter(percentage >0.1) %>% 
    as.data.frame()
  regio_df <- regio_df[order(regio_df$percentage,decreasing = T),]
  regio_df <- left_join(regio_df,colourscheme)
  
  ggplot(regio_df) +
    geom_bar(aes(x = "", y = percentage, fill = party),
             stat = "identity", colour = "black", width =1) +
    #geom_text(aes(x = "", y = pct, label = percent(pct)), position = position_stack(vjust = 0.5))+
    coord_polar("y", start = 0) +
    labs(x = NULL, y = NULL, fill = NULL, 
         title = paste("Verkiezingsuitslag",unique(regio_df$regio))) +
    theme_classic()+
    scale_fill_manual(values = regio_df$colour, 
                      limits = regio_df$party)+ 
    theme(axis.line = element_blank(),
          axis.text = element_blank(),
          axis.ticks = element_blank(),
          plot.title = element_text(hjust = 0.5, color = "black"),
          legend.position = "bottom")
})

#remove saba, st eustasius etc.
df_long <- df_long %>% 
  filter(!code %in% names_remove)

#link to spatial
shp_fptp <- left_join(shp_gemeentes, df_long, by = c("Gemeenteco"="code"))
shp_fptp <- shp_fptp %>% 
  filter(percentage >0.1)
#I need to make the shapefile alphabetical to match with the pie chart list
#This means removing apostrophe's at 's-Gravenhage and 's-Hertogenbosch
shp_fptp$Gemeentena <- gsub("'","",shp_gemeentes$Gemeentena)
#reorder
shp_fptp <- shp_fptp[order(shp_fptp$Gemeentena),]
#reset rownumbers so they line up
rownames(shp_fptp) <- 1:nrow(shp_fptp)
mapview(shp_fptp,
        zcol = "party",
        col.regions = shp_fptp$colour,
        popup = popupGraph(pie_plot_list, width = 450,height =300))